using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Collections;
using System.Runtime.InteropServices;
using System.Diagnostics;
using System.Windows.Forms;
using System.Threading;
using System.Collections.Generic;

using DirectShowLib;
using DirectShowLib.DES;
using OurMediaPlayer.Logic;
using OurMediaPlayer.Logic.Filters;

namespace OurMediaPlayer
{
    public delegate void MediaCoreEvent(Object sender);

    public class Media : IDisposable
    {
        // Enumeration of File Types
        public enum FileType
        {
            Video,
            Audio,
            Invalid
        }

        // Enumeration of Graph States
        public enum GraphState
        {
            Stopped,
            Paused,
            Running,
            Exiting
        }

        public FileType fileType = FileType.Invalid;

        // graph builder interfaces
        public IGraphBuilder graphBuilder = null;
        public IMediaControl mediaControl = null;
        public IVideoWindow videoWindow = null;
        public IBasicVideo basicVideo = null;
        public ISampleGrabber sampleGrabber = null;
        public IMediaSeeking mediaSeeking = null;
        public IMediaPosition mediaPosition = null;
        public IMediaDet mediaDet = null;
        public IBasicAudio basicAudio = null;
        public IMediaEventEx mediaEvent;

        public Control control;
        public MediaInfo mediaInfo = new MediaInfo();
        public List<OurImage> segmentationImages;

        private string filename;

        // Event used by Media Event thread
        private ManualResetEvent manualResetEvent = null;

        // Current state of the graph (can change async)
        volatile private GraphState currentState = GraphState.Stopped;

        // Event that is called when a clip finishs playing
        public event MediaCoreEvent StopPlay;

        // Dispose
        public void Dispose()
        {
            CloseInterfaces();
        }

        // Destructor
        ~Media()
        {
            CloseInterfaces();
        }

        public Media(string fileName)
        {
            try
            {
                // Event Handler
                IntPtr eventHandler;

                this.filename = fileName;

                // Set up Interfaces
                this.SetupInterfaces(fileName);

                // Get the event handle the graph will use to signal when events occur
                DsError.ThrowExceptionForHR(mediaEvent.GetEventHandle(out eventHandler));

                // Wrap the graph event with a ManualResetEvent
                manualResetEvent = new ManualResetEvent(false);

                manualResetEvent.SafeWaitHandle = new Microsoft.Win32.SafeHandles.SafeWaitHandle(eventHandler, true);

                // Create a new thread to wait for events
                Thread t = new Thread(new ThreadStart(this.EventWait));
                t.Name = "MediaCore Event Thread";
                t.Start();
            }
            catch
            {
                Dispose();
                throw;
            }
        }

        public void Load(double interval)
        {
            double fps = this.getFramePerSecond();

            double duration = this.getDuration();

            List<OurImage> list = new List<OurImage>();

            if (fileType == FileType.Video)
            {
                Bitmap snapshot;
                for (double i = 0; i < duration; i = i + interval)
                {
                    snapshot = this.SnapShot(i);
                    list.Add(new OurImage(i, snapshot));
                }
            }

            this.segmentationImages = list;
        }

        public void SetupInterfaces(string fileName)
        {
            this.graphBuilder = (IGraphBuilder)new FilterGraph();

            // create the SampleGrabber filter and config it
            this.sampleGrabber = (ISampleGrabber)new SampleGrabber();
            ConfigSampleGrabber(this.sampleGrabber);
            this.graphBuilder.AddFilter((IBaseFilter)sampleGrabber, "SampleGrabber");

            // Have the graph builder construct its the appropriate graph automatically
            DsError.ThrowExceptionForHR(this.graphBuilder.RenderFile(fileName, null));

            // QueryInterface for DirectShow interfaces
            this.mediaControl = (IMediaControl)this.graphBuilder;
            this.mediaEvent = (IMediaEventEx)this.graphBuilder;
            this.mediaSeeking = (IMediaSeeking)this.graphBuilder;
            this.mediaPosition = (IMediaPosition)this.graphBuilder;

            // Query for video interfaces, which may not be relevant for audio files
            this.videoWindow = this.graphBuilder as IVideoWindow;
            this.basicVideo = this.graphBuilder as IBasicVideo;
            this.basicAudio = this.graphBuilder as IBasicAudio;
            this.mediaDet = (IMediaDet)new MediaDet();
            this.mediaDet.put_Filename(fileName);

            // Checks file type
            this.CheckFileType();

            // Gets information about video
            if (this.fileType == FileType.Video)
                this.GetMediaInfo();

            // TODO: this.videoWindow.put_MessageDrain(((MainForm)this.control).moviePanel.Handle);

            // Configures Video Window
            // TODO: ConfigureVideoWindow(control);
        }

        public void SetupVideoWindow(Control control)
        {
            // Inicialization
            int hr = 0;

            this.control = control;

            if (this.fileType == FileType.Video)
            {
                // we use BufferCB (i.e. 1) because we couldn't make SampleCB works
                this.sampleGrabber.SetCallback(((MainForm)this.control.Parent), 1);

                // Gets the video Info
                this.GetMediaInfo();

                // Set the output window
                hr = this.videoWindow.put_Owner(control.Handle);
                DsError.ThrowExceptionForHR(hr);

                // Set the window style
                hr = this.videoWindow.put_WindowStyle((WindowStyle.Child | WindowStyle.ClipChildren | WindowStyle.ClipSiblings));
                DsError.ThrowExceptionForHR(hr);

                // Make the window visible
                hr = this.videoWindow.put_Visible(OABool.True);
                DsError.ThrowExceptionForHR(hr);

                hr = this.videoWindow.put_MessageDrain(control.Handle);

                // Size Windows 
                this.SizeWindow();

                DsError.ThrowExceptionForHR(hr);
            }
        }

        public void ClearVideoWindow()
        {
            // Inicialization
            int hr = 0;

            if (this.fileType == FileType.Video)
            {
                // Set the output window
                hr = this.videoWindow.put_Owner(IntPtr.Zero);
                DsError.ThrowExceptionForHR(hr);

                // Make the window visible
                hr = this.videoWindow.put_Visible(OABool.False);
                DsError.ThrowExceptionForHR(hr);

                hr = this.videoWindow.put_MessageDrain(IntPtr.Zero);

                DsError.ThrowExceptionForHR(hr);
            }
        }

        private void CheckFileType()
        {
            int hr;

            if ((this.videoWindow != null) || (this.basicVideo != null))
                this.fileType = FileType.Video;
            else if ((this.basicAudio != null))
                this.fileType = FileType.Audio;
            else
                this.fileType = FileType.Invalid;

            OABool lVisible;
            hr = this.videoWindow.get_Visible(out lVisible);
            if (hr < 0)
            {
                if (hr == unchecked((int)0x80004002)) //E_NOINTERFACE
                    this.fileType = FileType.Audio;
                else
                    DsError.ThrowExceptionForHR(hr);
            }
        }

        private void EventWait()
        {
            int hr;
            int firstParameter, secondParameter;
            EventCode eventCode;

            do
            {
                // Wait for an event
                manualResetEvent.WaitOne(-1, true);

                // Avoid contention for m_State
                lock (this)
                {
                    // If we are not shutting down
                    if (currentState != GraphState.Exiting)
                    {
                        // Read the event
                        hr = mediaEvent.GetEvent(out eventCode, out firstParameter, out secondParameter, 0);

                        // If there's an event to attend to
                        while (hr >= 0)
                        {
                            // Write the event name to the debug window
                            // Debug.WriteLine(eventCode.ToString());

                            // If the clip is finished playing
                            if (eventCode == EventCode.Complete)
                            {
                                // Call Stop() to set state
                                Stop();

                                // Throw event
                                if (StopPlay != null)
                                    StopPlay(this);
                            }

                            // Release any resources the message allocated
                            hr = mediaEvent.FreeEventParams(eventCode, firstParameter, secondParameter);
                            DsError.ThrowExceptionForHR(hr);

                            // Try to get next event
                            hr = mediaEvent.GetEvent(out eventCode, out firstParameter, out secondParameter, 0);
                        }

                        // If the error that exited the loop wasn't due to running out of events
                        if (hr != unchecked((int)0x80004004))
                            DsError.ThrowExceptionForHR(hr);

                    }
                    else
                        // We are shutting down
                        break;
                }
            } while (true);
        }

        public void SizeWindow()
        {
            int hr;

            Rectangle rc = control.ClientRectangle;
            int windowWidth, windowHeight;
            windowWidth = rc.Width;
            windowHeight = rc.Height;

            int videoWidth = this.mediaInfo.MediaWidth;
            int videoHeight = this.mediaInfo.MediaHeight;

            int x, y, width, height;

            double videoRatio = (double)videoWidth / (double)videoHeight;
            double windowRatio = (double)windowWidth / (double)windowHeight;

            if (videoRatio > windowRatio)
            {
                double ratio = (double)windowWidth / (double)videoWidth;
                x = (int)((windowWidth - videoWidth * ratio) / 2);
                y = (int)((windowHeight - videoHeight * ratio) / 2);
                width = (int)(videoWidth * ratio);
                height = (int)(videoHeight * ratio);
                hr = videoWindow.SetWindowPosition(x, y, width, height);
            }
            else
            {
                double ratio = (double)windowHeight / (double)videoHeight;
                x = (int)((windowWidth - videoWidth * ratio) / 2);
                y = (int)((windowHeight - videoHeight * ratio) / 2);
                width = (int)(videoWidth * ratio);
                height = (int)(videoHeight * ratio);
                hr = videoWindow.SetWindowPosition(x, y, width, height);
            }

            DsError.ThrowExceptionForHR(hr);
        }

        void ConfigSampleGrabber(ISampleGrabber sampGrabber)
        {
            AMMediaType media;

            // set the media type 
            media = new AMMediaType();
            media.majorType = MediaType.Video;
            media.subType = MediaSubType.RGB24;
            media.formatType = FormatType.VideoInfo;

            // that's the call to the ISampleGrabber interface
            this.sampleGrabber.SetMediaType(media);

            DsUtils.FreeAMMediaType(media);
            media = null;

            // Configure the samplegrabber
            int hr = sampGrabber.SetBufferSamples(true);
            DsError.ThrowExceptionForHR(hr);
        }

        private void GetMediaInfo()
        {
            AMMediaType media = new AMMediaType();

            // GetConnectedMediaType retrieve the media type for a sample
            this.sampleGrabber.GetConnectedMediaType(media);

            if ((media.formatType != FormatType.VideoInfo) || (media.formatPtr == IntPtr.Zero))
            {
                throw new Exception("Format type incorrect");
            }

            int videoWidth, videoHeight, videoStride;
            this.basicVideo.GetVideoSize(out videoWidth, out videoHeight);

            // save the stride
            VideoInfoHeader videoInfoHeader = (VideoInfoHeader)Marshal.PtrToStructure(media.formatPtr, typeof(VideoInfoHeader));
            videoStride = videoWidth * (videoInfoHeader.BmiHeader.BitCount / 8);

            this.mediaInfo.MediaWidth = videoWidth;
            this.mediaInfo.MediaHeight = videoHeight;
            this.mediaInfo.MediaStride = videoStride;
            this.mediaInfo.MediaBpp = videoInfoHeader.BmiHeader.BitCount;

            DsUtils.FreeAMMediaType(media);
            media = null;
        }

        // Takes a snaptshot
        public IntPtr SnapShot()
        {
            int hr;
            IntPtr ip = IntPtr.Zero;
            int iBuffSize = 0;

            // Read the buffer size
            hr = this.sampleGrabber.GetCurrentBuffer(ref iBuffSize, ip);
            DsError.ThrowExceptionForHR(hr);

            // Allocate the buffer and read it
            ip = Marshal.AllocCoTaskMem(iBuffSize);

            hr = this.sampleGrabber.GetCurrentBuffer(ref iBuffSize, ip);
            DsError.ThrowExceptionForHR(hr);

            return ip;
        }

        public Bitmap SnapShot(double position)
        {
            int hr;
            IntPtr ip = IntPtr.Zero;
            int iBuffSize;
            hr = this.mediaDet.GetBitmapBits(position, out iBuffSize, ip, this.mediaInfo.MediaWidth, this.mediaInfo.MediaHeight);
            ip = Marshal.AllocCoTaskMem(iBuffSize);
            hr = this.mediaDet.GetBitmapBits(position, out iBuffSize, ip, this.mediaInfo.MediaWidth, this.mediaInfo.MediaHeight);

            Bitmap bm = new Bitmap(this.mediaInfo.MediaWidth, this.mediaInfo.MediaHeight);
            try
            {
                bm = new Bitmap(
                    this.mediaInfo.MediaWidth,
                    this.mediaInfo.MediaHeight,
                    -this.mediaInfo.MediaStride,
                    PixelFormat.Format24bppRgb,
                    (IntPtr)(ip.ToInt32() + iBuffSize - this.mediaInfo.MediaStride)
                    );
            }catch(Exception e)
            {
                Console.Out.WriteLine("Could not convert bitmapbits to bitmap: " + e.Message);
            }

            return bm;
        }

        // Convert a point to the raw pixel data to a .NET bitmap
        public Bitmap IPToBmp(IntPtr ip, MediaInfo mediaInfo)
        {
            // We know the Bits Per Pixel is 24 (3 bytes) because we forced it 
            // to be with sampGrabber.SetMediaType()
            int iBufSize = mediaInfo.MediaWidth * mediaInfo.MediaHeight * 3;

            return new Bitmap(
                mediaInfo.MediaWidth,
                mediaInfo.MediaHeight,
                -mediaInfo.MediaStride,
                PixelFormat.Format24bppRgb,
                (IntPtr)(ip.ToInt32() + iBufSize - mediaInfo.MediaStride)
                );
        }

        // CloseInterfaces
        private void CloseInterfaces()
        {
            lock (this)
            {
                int hr;
                try
                {
                    if (this.mediaControl != null)
                    {
                        hr = this.mediaControl.StopWhenReady();
                        this.mediaControl = null;
                    }

                    // Exiting state
                    if (currentState != GraphState.Exiting)
                    {
                        currentState = GraphState.Exiting;

                        // Release the thread (if the thread was started)
                        if (manualResetEvent != null)
                            manualResetEvent.Set();
                    }

                    if (this.videoWindow != null)
                    {
                        hr = this.videoWindow.put_Visible(OABool.False);
                        hr = this.videoWindow.put_Owner(IntPtr.Zero);
                        this.videoWindow = null;
                    }

                    this.mediaSeeking = null;
                    this.mediaPosition = null;
                    this.basicVideo = null;
                    this.basicAudio = null;
                    this.mediaDet = null;

                    if (this.graphBuilder != null)
                        Marshal.ReleaseComObject(this.graphBuilder);
                    this.graphBuilder = null;
                }
                catch (Exception e)
                {
                    MessageBox.Show(e.Message);
                    return;
                }
            }

            // Runs garbage collector
            GC.Collect();
        }

        // Rewinds the movie file
        public void Rewind()
        {
            DsError.ThrowExceptionForHR(this.mediaPosition.put_CurrentPosition(0));
        }

        // FastBackwards the capture graph.
        public void FastBackwards()
        {
            // Can only Rewind when playing or paused
            if (this.currentState == GraphState.Running || this.currentState == GraphState.Paused)
            {
                // Pauses the playback
                DsError.ThrowExceptionForHR(mediaControl.Pause());

                // Sets the current state to stopped
                currentState = GraphState.Paused;
            }

            double fps, currentPosition;

            // Retrieves the fps and current position
            fps = this.getFramePerSecond();
            this.mediaPosition.get_CurrentPosition(out currentPosition);

            // Updates the current movie position
            double newPosition = currentPosition - 1 / fps;
            this.mediaPosition.put_CurrentPosition(Math.Max(0, newPosition));
        }

        // Starts the capture graph
        public void Start()
        {
            // If we aren't already playing (or shutting down)
            if (this.currentState == GraphState.Stopped || this.currentState == GraphState.Paused)
            {
                // Starts playback
                DsError.ThrowExceptionForHR(mediaControl.Run());

                // Sets current state to Running
                this.currentState = GraphState.Running;
            }
        }

        // Pauses the capture graph
        public void Pause()
        {
            // If we are playing
            if (this.currentState == GraphState.Running)
            {
                // Pauses the playback
                DsError.ThrowExceptionForHR(mediaControl.Pause());

                // Sets the current state to paused
                this.currentState = GraphState.Paused;
            }
        }

        // Pause the capture graph.
        public void Stop()
        {
            // Can only Stop when playing or paused
            if (this.currentState == GraphState.Running || this.currentState == GraphState.Paused)
            {
                // Stops the playback
                DsError.ThrowExceptionForHR(mediaControl.Stop());

                // Sets the current state to stopped
                currentState = GraphState.Stopped;
            }
        }

        // FastForwards the capture graph.
        public void FastForward()
        {
            // Can only FastForward when playing or paused
            if (this.currentState == GraphState.Running || this.currentState == GraphState.Paused)
            {
                // Pauses the playback
                DsError.ThrowExceptionForHR(mediaControl.Pause());

                // Sets the current state to stopped
                currentState = GraphState.Paused;
            }

            // Initialize variables
            double fps, currentPosition, duration;

            // Gets the fps, current position and duration of the stream
            fps = this.getFramePerSecond();
            this.mediaPosition.get_CurrentPosition(out currentPosition);
            this.mediaPosition.get_Duration(out duration);

            double newPosition = currentPosition + 1 / fps;
            this.mediaPosition.put_CurrentPosition(Math.Min(duration, newPosition));
        }

        // Rewinds the movie file
        public void End()
        {
            // Inicializes variables
            double endPosition = 0;

            // Gets the stream length of the video
            this.mediaDet.get_StreamLength(out endPosition);

            // Sets the movie to the last Position
            DsError.ThrowExceptionForHR(mediaPosition.put_CurrentPosition(endPosition));


        }

        public double getFramePerSecond()
        {
            double result = 0;

            this.mediaDet.get_FrameRate(out result);
            if (result != 0)
                return result;

            this.basicVideo.get_AvgTimePerFrame(out result);
            if (result != 0)
                return 1 / result;

            if (result == 0)
                result = 25.0;

            return result;
        }

        public double getPosition()
        {
            double result = 0;

            this.mediaPosition.get_CurrentPosition(out result);

            return result;
        }

        public double getDuration()
        {
            double result = 0;

            this.mediaPosition.get_Duration(out result);

            return result;
        }

        public void setVolume(int volume)
        {
            this.basicAudio.put_Volume(volume);
        }

        public void setPosition(int frame)
        {
            double fps = this.getFramePerSecond();

            double currentPosition = frame / fps;

            this.mediaPosition.put_CurrentPosition(currentPosition);
        }

        public void setPosition(double position)
        {
            // Initialize variables
            double duration;

            // Gets the duration of the stream;
            this.mediaPosition.get_Duration(out duration);

            // Garantees the new value is within movie boundaries
            double newPosition = Math.Max(0, Math.Min(duration, position));

            // Sets the position
            this.mediaPosition.put_CurrentPosition(newPosition);
        }
    }
}
